/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package eu.anorien.dmo.gui.optimization;

import eu.anorien.dmo.Function;
import eu.anorien.dmo.Parameter;
import eu.anorien.dmo.Server;
import eu.anorien.dmo.gui.Optimizations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.BoxLayout;
import javax.swing.JTabbedPane;
import org.apache.log4j.Logger;

/**
 *
 * @author David Miguel Antunes <davidmiguel [ at ] antunes.net>
 */
public abstract class Optimization extends javax.swing.JPanel {

    private static final Logger logger = Logger.getLogger(Optimization.class);
    protected ArrayList<Thread> threads = new ArrayList<Thread>();
    protected long parameterSpaceSize = 1;
    protected Random random = new Random(58634);
    protected PerformanceGraph graphPanel = new PerformanceGraph();
    protected Status statusPanel = new Status();
    protected Data dataPanel = new Data();
    protected JTabbedPane tabbedPane = new JTabbedPane();
    protected Optimizations optimizations;
    protected Function function;
    protected ArrayList<Server> servers;
    protected Output output;
    protected double smallestError = Double.MAX_VALUE;
    protected Object[] bestParams = null;
    protected long startTime;
    protected double averageRunningTime = 0;
    protected double averageRunningTimeUpdate = 0.5;
    protected int nRuns = 0;
    protected int nCacheHits = 0;
    protected boolean terminate = false;
    protected int stopAfter = 0;
    private final HashMap<ArrayList<Object>, Double> errorValueCache = new HashMap<ArrayList<Object>, Double>();

    public Optimization(Function function, ArrayList<Server> servers) {
        this.function = function;
        this.servers = servers;
        this.output = new Output(this);
        this.setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
        add(tabbedPane);
        tabbedPane.add("Output", output);
        tabbedPane.add("Status", statusPanel);
        tabbedPane.add("Performance graph", graphPanel);
        tabbedPane.add("Raw performance data", dataPanel);
        new Timer().schedule(new TimerTask() {

            @Override
            public void run() {
                updateStatus();
            }
        }, 0, 1000);
    }

    private void updateStatus() {
        statusPanel.setAverageTimeToRun((long) averageRunningTime);
        if (bestParams != null) {
            statusPanel.setBestParams(paramsToString(bestParams));
        }
        statusPanel.setBestPerformance(smallestError);
        statusPanel.setRunningTime(System.currentTimeMillis() - startTime);
        statusPanel.setCacheHits(nCacheHits);
        statusPanel.setNumRuns(nRuns);
        statusPanel.setAverageRunsPerMinute(((1000 * 60) * nRuns) / (System.currentTimeMillis() - startTime));
        statusPanel.setPercentParamSpcaeExploredLabel(nRuns / (double) parameterSpaceSize);
    }

    protected double runFunction(Object[] params) {
        nRuns++;
        double error = -2;
        synchronized (errorValueCache) {
            if (errorValueCache.get(paramsToArrayList(params)) != null) {
                error = errorValueCache.get(paramsToArrayList(params));
                nCacheHits++;
                output.write("Cache hit with params: [" + paramsToString(params) + "]");
            }
        }
        if (error == -2) {
            output.write("Running with params: [" + paramsToString(params) + "]");
            long t = System.currentTimeMillis();
            error = ServerPool.getInstance().runFunction(function.getName(),
                    paramsToString(params));
            long elapsed = System.currentTimeMillis() - t;
            if (error == -1) {
                error = Double.MAX_VALUE;
                output.write("Invalid params: [" + paramsToString(params) + "]");
            }
            synchronized (errorValueCache) {
                errorValueCache.put(paramsToArrayList(params), error);
            }
            averageRunningTime = averageRunningTime * averageRunningTimeUpdate + elapsed * (1 - averageRunningTimeUpdate);
            output.write("Performance of " + (1 / error) + " with params: \'" + paramsToString(params) + "\' in " + (elapsed / 1000) + " secs");
            newErrorValue(params, error);
        }
        if (stopAfter != 0 && nRuns == stopAfter) {
            stop();
        }
        return error;
    }

    private void newErrorValue(Object[] paramsArray, double error) {
        if (smallestError > error) {
            output.write("New best performance of " + (1 / smallestError) + " with params: \'" + paramsToString(paramsArray) + "\'");
            smallestError = error;
            bestParams = paramsArray;
            graphPanel.newSmallestError(System.currentTimeMillis() - startTime, smallestError);
            dataPanel.newSmallestError(System.currentTimeMillis() - startTime, nRuns, paramsToString(paramsArray), smallestError);
        }
        graphPanel.newRun(System.currentTimeMillis() - startTime, error);
        // This is for keeping the best parameters with a continuous line:
        graphPanel.newSmallestError(System.currentTimeMillis() - startTime, smallestError);

        dataPanel.newRun(System.currentTimeMillis() - startTime, nRuns, paramsToString(paramsArray), error);
    }

    public void setOptimizations(Optimizations optimizations) {
        this.optimizations = optimizations;
    }

    protected ArrayList<Integer> newRandomPoint() {
        ArrayList<Integer> point = new ArrayList<Integer>();
        for (Parameter parameter : function.getParameters()) {
            point.add(random.nextInt(parameter.getNSteps()));
        }
        return point;
    }

    protected Object[] pointToParams(ArrayList<Integer> point) {
        Object[] params = new Object[point.size()];
        List<Parameter> parameters = function.getParameters();
        int i = 0;
        for (Integer integer : point) {
            params[i] = parameters.get(i).getValue(integer);
            i++;
        }
        return params;
    }

    private ArrayList<Object> paramsToArrayList(Object[] params) {
        ArrayList<Object> list = new ArrayList<Object>();
        list.addAll(Arrays.asList(params));
        return list;
    }

    protected String paramsToString(Object[] params) {
        String rslt = "";
        if (params.length > 0) {
            rslt += params[0];
        }
        for (int i = 1; i < params.length; i++) {
            if (params[i] instanceof Double) {
                rslt += ", " + new String().format("%.4f", params[i]);
            } else {
                rslt += ", " + params[i];
            }
        }
        return rslt;
    }

    public void start() {
        startTime = System.currentTimeMillis();
        function.setLocked(true);
        optimizations.updateOtherPanels();
        parameterSpaceSize = 1;
        for (Parameter parameter : function.getParameters()) {
            parameterSpaceSize *= parameter.getNSteps();
        }
        output.write("Parameter space size is: " + parameterSpaceSize);
        tabbedPane.setSelectedIndex(2);
    }

    public void stop() {
        tabbedPane.setSelectedIndex(1);
        output.write("Terminating optimization...");
        terminate = true;
        for (Thread thread : threads) {
            thread.interrupt();
        }
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException ex) {
            }
        }
        output.write("Optimization FINISHED.");
        function.setLocked(false);
    }

    ArrayList<Server> getServers() {
        return servers;
    }
}
